Skip to content

feat: fuzzy regression discontinuity via treatment_col on RegressionDiscontinuity#686

Merged
igerber merged 5 commits into
mainfrom
rdd-fuzzy
Jul 13, 2026
Merged

feat: fuzzy regression discontinuity via treatment_col on RegressionDiscontinuity#686
igerber merged 5 commits into
mainfrom
rdd-fuzzy

Conversation

@igerber

@igerber igerber commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds FUZZY regression discontinuity to RegressionDiscontinuity as a fit-time option - fit(data, outcome_col, running_col, treatment_col=...) (R's fuzzy=); None remains the sharp design. No new estimator class; estimator count unchanged.
  • Estimand: the local Wald ratio, with a data-dependent estimand label - "fuzzy (LATE for compliers at the cutoff)" for binary take-up, "fuzzy (local Wald ratio at the cutoff; non-binary take-up)" otherwise, so the label never overclaims.
  • rdrobust 4.0.0 parity end-to-end: T stacked as a second response column through the same local-polynomial fits, LINEARIZED bias correction (tau_bc = tau_cl - s_Y . B_F), delta-method variances with the Y-T covariance via the res @ s_Y collapse, first-stage variance via sV_T = [0, 1], and the linearized per-side biases.
  • Fuzzy bandwidth logic (CCFT 2017 Section 6, R-exact): fuzzy-ratio objective by default (T threaded through all three selector chains of the port), sharpbw=False constructor knob, and the one-sided perfect-compliance auto-switch to the sharp reduced form.
  • First stage is first-class: full three-row first_stage* mirror (robust-row canonical binding like att), a first-stage block in summary(), and per-side take-up coefficients (rdplot seam). All None on sharp fits; to_dict() splits None-safe.
  • Weak-first-stage UserWarning when the first-stage robust CI is finite and contains zero (documented deviation - R is verified silent; CCT 2014 Theorem 3 "guard and warn"); R's exact no-variation-no-jump identification error raised on both entry points, ordered before mass-point detection as in R.
  • Port functions renamed to the module's R-name convention now that both designs run through them (rdbwselect, rdrobust_fit, rdrobust_vce, RdFitResult); sharp t=None paths unchanged (all pre-existing goldens reproduce value-exact).

Methodology references (required if estimator / math changes)

  • Method name(s): Fuzzy regression discontinuity (local Wald ratio) with robust bias-corrected inference; fuzzy MSE-optimal bandwidth selection with sharp-reduced-form switching.
  • Paper / source link(s): Calonico, Cattaneo & Titiunik (2014), Econometrica 82(6), doi:10.3982/ECTA11757 (Section 3.2, Theorems 3-4); Calonico, Cattaneo, Farrell & Titiunik (2017), Stata Journal 17(2) (fuzzy bandwidth approaches, sharpbw, perfect-compliance auto-switch); Feir, Lemieux & Marmer (2016), JBES 34(2) (weak identification - warning rationale; weak-IV-robust inference a documented seam). Parity pin: CRAN rdrobust 4.0.0 tarball (sha256 in diff_diff/_rdrobust_port.py).
  • Any intentional deviations from the source (and why): each labeled in docs/methodology/REGISTRY.md (RegressionDiscontinuity section) - weak-first-stage warning with a finite-CI gate (R silent; no-silent-failures policy), first-stage se=0 NaN-gating under perfect compliance (R prints z=Inf), fail-closed degenerate-pilot error (R flows Inf/NaN), warn-and-ignore sharpbw on sharp fits (R silent), data-dependent estimand labeling.

Validation

  • Tests added/updated: tests/test_rdd_parity.py (23-config R golden parity at rtol=1e-9 incl. 7 fuzzy configs with full first-stage three-row pins - default/sharpbw/manual-h/epa/msetwo/one-sided-compliance/ties), tests/test_rdrobust_port.py (port-level fuzzy parity incl. the linearized per-side bias pins + fuzzy input validation), tests/test_rdd_methodology.py (perfect-compliance == sharp reproduction, perf_comp/sharpbw bandwidth switch locks, outcome-scaling equivariance, R-exact identification error ordering, weak/strong first-stage warning gate, degenerate-pilot fail-closed, N<20 fuzzy fallback, constant-outcome contracts), tests/test_rdd.py (treatment_col API, estimand labels incl. non-binary, first-stage None-on-sharp, sharpbw plumbing).
  • Backtest / simulation / notebook evidence (if applicable): R golden fixtures regenerated from pinned CRAN rdrobust 4.0.0 (benchmarks/R/generate_rdrobust_estimates_golden.R); all 16 pre-existing configs reproduced value-exact; worst observed fuzzy parity deviation ~3.3e-11 (bandwidths ~5.9e-13 across all 10 selectors).

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

Generated with Claude Code

https://claude.ai/code/session_01QGca52n6H8oDDXALjjrsp4

…iscontinuity

Local Wald ratio estimand with rdrobust 4.0.0 parity end-to-end: linearized
bias correction and delta-method variances (T stacked as a second response
column), fuzzy-ratio bandwidth objective with R's sharpbw / one-sided
perfect-compliance auto-switch, full first-stage three-row mirror +
summary block, weak-first-stage warning (documented deviation - R is
silent), and R's exact identification error. 7 new fuzzy golden configs
(23 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGca52n6H8oDDXALjjrsp4
@github-actions

Copy link
Copy Markdown

Overall Assessment

✅ Looks good. No unmitigated P0/P1 findings.

Executive Summary

  • Methodology changes affect RegressionDiscontinuity fuzzy RD and the private rdrobust port.
  • Fuzzy local Wald ratio, linearized bias correction, first-stage variance, sharpbw, and weak-first-stage behavior are documented in docs/methodology/REGISTRY.md and reflected in code.
  • New inference fields use safe_inference(); I did not find new inline t_stat = effect / se anti-patterns in modified RD code.
  • One P3 documentation stale reference remains outside the changed-file list.
  • I could not run the targeted pytest suite because this environment lacks pytest and even numpy.

Methodology

No unmitigated findings.

Cross-check: fuzzy ratio assembly, linearized s_Y . B_F bias correction, first-stage sV_T, and Y-T covariance via residual collapse are implemented in diff_diff/_rdrobust_port.py:L1262-L1310 and match the registry contract in docs/methodology/REGISTRY.md:L3530-L3554. Documented deviations such as weak-first-stage warning and first-stage se=0 NaN-gating are explicitly labeled in docs/methodology/REGISTRY.md:L3641-L3667.

Code Quality

No findings.

Performance

No findings.

Maintainability

No findings.

Tech Debt

No findings. Deferred limitations such as covariates, cluster variance, weak-IV-robust fuzzy inference, and diagnostics are documented in the registry.

Security

No findings.

Documentation/Tests

P3 — Stale internal documentation reference

  • Location: benchmarks/R/README.md:L140
  • Impact: The benchmark README still points to diff_diff._rdrobust_port.rdbwselect_sharp, but the PR renamed the port entry point to rdbwselect. This is documentation-only; no code callers remain.
  • Concrete fix: Update the reference to diff_diff._rdrobust_port.rdbwselect and, if desired, mention that the port now covers sharp and fuzzy RD bandwidth paths.

Verification note: attempted python -m pytest tests/test_rdd.py tests/test_rdd_methodology.py tests/test_rdd_parity.py tests/test_rdrobust_port.py -q -p no:cacheprovider, but the environment has no pytest; direct import checks also failed because numpy is unavailable.

The port entry point was renamed to rdbwselect when fuzzy support landed;
this internal README reference was the one site the rename sweep missed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGca52n6H8oDDXALjjrsp4
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: e44dcde0244c166a650d6579dffb89d789902e78


Overall Assessment

✅ Looks good. No unmitigated P0/P1 findings.

Executive Summary

  • Affected method: RegressionDiscontinuity fuzzy RD and the private rdrobust port.
  • Methodology matches the registry: local Wald ratio, linearized fuzzy bias correction, delta-method variance with Y-T covariance, first-stage rows, sharpbw, and documented weak-first-stage deviation are all covered.
  • New inference rows use safe_inference(); I did not find new inline t_stat = effect / se anti-patterns in modified RD code.
  • The previous stale reference to rdbwselect_sharp is fixed.
  • One P3 documentation wording issue remains in the benchmark README.
  • Targeted tests could not be run because this environment lacks both pytest and numpy.

Methodology

No findings.

Cross-check: the registry specifies fuzzy RD as the local Wald ratio with linearized tau_bc = tau_cl - s_Y . B_F, residual collapse via res @ s, first-stage variance via sV_T, and fuzzy bandwidth selection via the ratio objective unless sharpbw or perfect-compliance switching applies (docs/methodology/REGISTRY.md:L3530-L3579). The port implements those pieces in diff_diff/_rdrobust_port.py:L277-L293, diff_diff/_rdrobust_port.py:L730-L742, and diff_diff/_rdrobust_port.py:L1262-L1310. Documented deviations for weak-first-stage warnings, first-stage se=0 NaN-gating, sharpbw on sharp fits, and identification-guard behavior are labeled in docs/methodology/REGISTRY.md:L3641-L3679.

Code Quality

No findings.

Performance

No findings.

Maintainability

No findings.

Tech Debt

No findings. Deferred RD seams are documented in the registry, including covariates, cluster variance, weights, kink estimands, and weak-IV-robust fuzzy inference (docs/methodology/REGISTRY.md:L3680-L3685).

Security

No findings.

Documentation/Tests

P3 — Benchmark README overstates fuzzy coverage in rdrobust_golden.json

  • Location: benchmarks/R/README.md:L137-L142
  • Impact: The README now implies generate_rdrobust_golden.R / rdrobust_golden.json verifies both sharp and fuzzy bandwidth paths across all 10 selectors. The registry says the fuzzy parity is actually in rdrobust_estimates_golden.json and TestFuzzyPortGoldenParity, while rdrobust_golden.json remains the 17-config sharp bandwidth matrix.
  • Concrete fix: Reword the sentence to say rdrobust_golden.json covers sharp bandwidth selection, and add a second sentence pointing fuzzy bandwidth/estimation parity to benchmarks/data/rdrobust_estimates_golden.json and tests/test_rdrobust_port.py::TestFuzzyPortGoldenParity.

Verification note: attempted PYTHONDONTWRITEBYTECODE=1 python -m pytest tests/test_rdd.py tests/test_rdd_methodology.py tests/test_rdd_parity.py tests/test_rdrobust_port.py -q -p no:cacheprovider, but pytest is not installed. Direct dependency check also showed numpy is unavailable.

rdrobust_golden.json is the frozen 17-config SHARP bandwidth fixture;
fuzzy bandwidth/estimation parity lives in rdrobust_estimates_golden.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGca52n6H8oDDXALjjrsp4
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: ea3eb1336bda9ce6a41761ff39e0d717357337e2


Overall Assessment

✅ Looks good. No unmitigated P0/P1 findings.

Executive Summary

  • Previous P3 benchmark README issue is resolved.
  • Affected method is RegressionDiscontinuity fuzzy RD plus the private rdrobust port.
  • Methodology matches the registry: local Wald ratio, linearized fuzzy bias correction, Y-T covariance via residual collapse, first-stage mirror, sharpbw, and weak-first-stage warning are documented and implemented.
  • No new inline inference anti-patterns found in changed RD code; inference uses safe_inference().
  • Could not run tests: pytest and numpy are not installed in this environment.

Methodology

No findings.

The fuzzy implementation matches docs/methodology/REGISTRY.md: T is stacked as a second response, tau_bc = tau_cl - s_Y . B_F, variance uses res @ s_Y, first-stage variance uses sV_T = [0, 1], and fuzzy bandwidth selection switches to sharp under sharpbw or one-sided perfect compliance. Documented deviations for weak-first-stage warnings and first-stage se=0 NaN-gating are clearly labeled, so they are not defects.

Code Quality

No findings.

Performance

No findings.

Maintainability

No findings.

Tech Debt

No findings.

Deferred RD seams remain documented in the registry, including covariates, cluster variance, weights, kink estimands, and weak-IV-robust fuzzy inference.

Security

No findings.

Documentation/Tests

P3 — Some user-facing prose still overstates fuzzy RD as complier LATE without the binary-takeup qualifier

  • Location: docs/api/regression_discontinuity.rst:L10-L14, diff_diff/rdd.py:L8-L13, diff_diff/guides/llms-full.txt:L876-L879
  • Impact: The code and registry correctly distinguish binary take-up (“LATE for compliers”) from non-binary take-up (“local Wald ratio”), but a few intro/example sentences still describe fuzzy RD as complier LATE without immediately saying “for binary take-up under monotonicity.” This is minor because the result estimand field and detailed fit docstring are correct.
  • Concrete fix: Qualify those sentences as “for binary take-up under monotonicity” and make the example comment say fuzzy.att # local Wald ratio; complier LATE only for binary take-up under monotonicity.

Verification

Attempted:

PYTHONDONTWRITEBYTECODE=1 python -m pytest tests/test_rdd.py tests/test_rdd_methodology.py tests/test_rdd_parity.py tests/test_rdrobust_port.py -q -p no:cacheprovider

Could not run because pytest is unavailable. A direct smoke import also failed because numpy is unavailable.

…verywhere

The estimand field and fit docstring were already conditional; the module
docstring intro, API page intro, and llms-full description/example now
carry the same binary-take-up-under-monotonicity qualifier.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGca52n6H8oDDXALjjrsp4
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 500ddb7c2f3e04fb7fffc0364ea4985e28a31750


Overall Assessment

✅ Looks good. No unmitigated P0/P1 findings.

Executive Summary

  • Affected methods: RegressionDiscontinuity fuzzy RD via treatment_col, plus the private rdbwselect / rdrobust_fit port.
  • Methodology matches docs/methodology/REGISTRY.md: local Wald ratio, linearized fuzzy bias correction, Y-T covariance via res @ s, first-stage mirror, sharpbw, and weak-first-stage warning are documented and implemented.
  • No new inline inference anti-pattern found; RD inference rows use safe_inference().
  • Previous documentation overclaim is mostly addressed; one residual P3 wording cleanup remains.
  • Could not run tests because pytest and numpy are unavailable in this environment.

Methodology

No findings.

The fuzzy RD implementation aligns with the registry and in-code references: T is stacked as a second response, tau_bc = tau_cl - s_Y . B_F, variance uses the collapsed two-column residual matrix, and first-stage variance uses sV_T = [0, 1] (diff_diff/_rdrobust_port.py:L1262-L1330, docs/methodology/REGISTRY.md:L3530-L3555). Documented deviations for weak-first-stage warnings, first-stage se=0 NaN-gating, and sharpbw behavior are labeled in the registry (docs/methodology/REGISTRY.md:L3641-L3668).

Code Quality

No findings.

Performance

No findings.

Maintainability

No findings.

Tech Debt

No findings.

Deferred RD seams remain documented: covariates, cluster-robust variance, weights, kink estimands, weak-IV-robust fuzzy inference, and diagnostics.

Security

No findings.

Documentation/Tests

P3 — Residual prose still says “complier LATE” without the binary-takeup qualifier in the same sentence

  • Location: CHANGELOG.md:L19-L22, diff_diff/rdd.py:L428-L431, diff_diff/guides/llms-full.txt:L876-L879
  • Impact: Minor documentation precision issue. The registry and detailed docs correctly say complier LATE applies for binary take-up under monotonicity, while non-binary take-up is only a local Wald ratio.
  • Concrete fix: Qualify these snippets as “complier LATE for binary take-up under monotonicity” or point directly to the data-dependent estimand label.

Verification

Attempted:

PYTHONDONTWRITEBYTECODE=1 python -m pytest tests/test_rdd.py tests/test_rdd_methodology.py tests/test_rdd_parity.py tests/test_rdrobust_port.py -q -p no:cacheprovider

Could not run: pytest is not installed. A direct dependency smoke check also failed because numpy is not installed.

CHANGELOG entry, class docstring, and doctest comment were the last
unqualified sites (repo-wide grep now clean).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGca52n6H8oDDXALjjrsp4
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 098515469034dae9e3a5df18f70313167054be30


Overall Assessment

✅ Looks good. No unmitigated P0/P1 findings.

Executive Summary

  • Affected methods: RegressionDiscontinuity fuzzy RD via fit(..., treatment_col=...), plus private rdbwselect, rdrobust_fit, and variance helpers.
  • Methodology matches the registry: local Wald ratio, linearized fuzzy bias correction, Y-T covariance in delta-method variance, first-stage mirror, sharpbw, and weak-first-stage warning are documented.
  • Prior P3 documentation wording issue appears resolved: “complier LATE” is now qualified for binary take-up/monotonicity or routed through the data-dependent estimand label.
  • No new inline inference anti-pattern found; RD result rows use safe_inference().
  • Could not run tests in this environment because pytest and numeric dependencies are unavailable.

Methodology

No findings.

Cross-check focused on fuzzy RD changes against docs/methodology/REGISTRY.md and in-code references. The implementation preserves the documented local Wald ratio, stacks treatment as a second response, uses linearized bias correction, includes Y-T covariance through the collapsed residual score, and reports first-stage inference consistently with the registry notes.

Relevant locations reviewed: diff_diff/_rdrobust_port.py:L277, diff_diff/_rdrobust_port.py:L505, diff_diff/_rdrobust_port.py:L1020, diff_diff/_rdrobust_port.py:L1262, diff_diff/rdd.py:L645-L918, docs/methodology/REGISTRY.md:L3530-L3680.

Code Quality

No findings.

Performance

No findings.

Maintainability

No findings.

Tech Debt

No findings.

Deferred seams such as covariates, cluster-robust variance, weights, kink estimands, weak-IV-robust fuzzy inference, and diagnostics are documented as non-blocking follow-ups.

Security

No findings.

Documentation/Tests

No findings.

The prior review’s P3 wording concern around “complier LATE” appears addressed in the changed prose. Verification was limited: tests could not be executed because the review environment lacks pytest, numpy, pandas, and scipy.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Jul 13, 2026
@igerber igerber merged commit eaa0255 into main Jul 13, 2026
38 of 39 checks passed
@igerber igerber deleted the rdd-fuzzy branch July 13, 2026 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant